소통하기: 인터랙티브 시각화

저자

이상일(서울대학교 지리교육과)

Modified

2025년 8월 26일

개요

여기서는 대시보드의 구성 요소로 활용할 수 있는 다양한 시각화 기법을 익힌다. 우리는 지금까지 도표(차트, 플롯, 그래프, 테이블)를 중심으로 데이터 시각화 기법을 다루었다. 특히 ggplot2 패키지는 이러한 과정에서 핵심적인 역할을 했다. 그런데 지금까지의 모든 도표는 정적(static) 이라는 특징이 있다. 많은 경우 도표는 정적으로 표현될 수 밖에 없으며, 또한 많은 경우 도표는 정적인 것으로 충분하며, 어떤 경우에는 동적인 것보다 더 낳다.

소통을 위한 시각화 재료로서 정적인 도표가 가지는 절대적인 중요성에도 불구하고, 상호작용성(interactivity)과 생동감(animatedness)이 부가된 도표는 어떤 상항에서는 소통의 본질적 가치를 고양하는데 많은 도움을 줄 수 있다.

더 나아가 지금까지는 시각적 도구로서 그래프에 집중한 경향이 있다. 시각적 도구로서 지도(maps)가 가지는 가치에도 불구하고 그래프에 비해 복잡한 측면이 있기 때문에 지금까지 다루지 않았다. 그러나, “지도는 텍스트, 테이블, 챠트와 같은 것들 보다 훨씬 더 효과적으로 정보를 전달할 수 있다.”(Dougherty 와/과 Ilyankou 2021)

gapminder 데이터를 실습의 여러 곳에서 활용할 것이다. 우선 tidyversegapminder패키지를 불러온다.

1 임베딩

동적, 반응형 시각화를 직접 제작하는 과정을 배우기 전에 대시보드에 동적, 반응형 시각화를 실현하는 가장 쉬운 방법은 동적, 반응형 시각화가 구현되어 있는 웹사이트를 대시보드에 불러오는 것일 것이다. 이것을 임베딩(embedding)이라고 하는데, HTML의 iframe 태그를 사용한다.

통계청의 통계놀이터는 다양한 주제에 대해 동적, 반응형 시각화를 제작하여 이용자들에게 제공하고 있다. 해당 홈페이지의 [비주얼 통계]에서 “우리나라 출생아 수와 합계 출산율의 변화”를 검색하면 동적, 반응형 시각화가 구현된 웹페이지를 볼 수 있다. [공유]를 눌러 URL을 복사하고, 아래와 같은 코드를 Quarto 문서에 삽입하면 해당 웹페이지를 임베딩할 수 있다. src="" 부분에 복사한 URL를 붙여 넣는다는 것을 쉽게 알 수 있을 것이다. 여기에 다른 URL을 교체해 넣으면 대부분의 웹사이트를 임베딩할 수 있는데, 임베딩이 불가능하게 막아 뒀거나 광고가 많이 붙어 있는 웹사이트는 잘 안된다. style="" 부분을 적절히 수정하면 임베딩된 웹사이트의 외견을 바꿔볼 수 있다.

<iframe src="https://kosis.kr/edu/share.do?shareID=S0500_16" 
loading="lazy" style="width: 100%; height: 600px; border: 
0px none;" allow="web-share; clipboard-write"></iframe>

Our World in Data는 아름다운 인터랙티브 시각화 자료를 제공하는 것으로 유명하다. 심지어 [공유] 버턴을 눌렀을 때 </> Embed 라는 옵션이 나타나는 데, 이것을 누르면 위와 같은 iframe 태그 내용이 그대로 나타나기 때문에 복사하여 붙이기만 하면 된다. Chart 탭 뿐만 아니라 Table 탭과 Map 탭도 있으니 눌러서 내용을 확인할 수 있다. 아마도 ECharts 자바스크립트 라이브러리를 사용한 것으로 보인다.

<iframe src="https://ourworldindata.org/grapher/child-mortality?time=earliest..latest&tab=chart" 
loading="lazy" style="width: 100%; height: 600px; border: 0px none;" allow="web-share; clipboard-write"></iframe>
그림 1: 임베딩 사례: Our World in Data

Our World in Data에서 제공하는 다양한 인터랙티브 시각화 자료를 임베딩하는 절차를 요약하면 다음과 같다.

  • Our World in Data에 접속한다.

  • 첫 화면의 하단에 있는 ’Data explorers’의 네 개 박스로 이동한다. 인구 관련 데이터에 관심이 있다면 ’Population & Demography’를 선택한다.

  • ’Population & Demography Data Explorer’에서 제공된 다양한 옵션을 이용해 원하는 인터랙티브 시각화 자료를 완성한다.

  • ‘Share’를 클릭하고’</> Embed’를 선택한 후 URL을 복사한다. 흥미로운 점은 위에서 어떤 옵션을 어떻게 설정했느냐에 따라 URL이 달라진다는 사실이다.

  • iframe 태그의 “src=”에 붙여 넣어 임베딩을 완수한다.

2 테이블

우리는 지금까지 테이블(table)의 중요성에 대해 거의 다루지 않았다. 그러나 상호작용형 테이블 혹은 대화형 테이블이 되었을 때, 많은 경우, 테이블은 가장 효과적인 정보 전달 도구가 된다. 특히, 데이터 변형하기를 통해 새로운 요약 테이블을 생성하고, 그것을 대화형으로 제시하는 것은 매우 중요한 데이터사이언스의 과정이다. 여기서는 DT 패키지를 활용하여 간단한 인터랙티브 테이블을 만들어 본다. 웹에서 인터렉티브 테이블을 생성할 수 있게 해주는 다양한 종류의 JavaScript 라이브러리가 존재한다. 예를 들어 DataTables, FlexTable, React Table 등이 있는데, DT 패키지는 DataTables를 쓸 수 있게 해주는 래퍼 패키지이다.

그리고 gapminder 데이터를 datatable() 함수를 통해 불러온다.

datatable(gapminder)

결과를 좀 더 크게 보기 위해, ‘Render’ 버튼 바로 오른편에 있는 아이콘을 클릭해 ’Chunk Output in Console’를 선택할 수 있다. 결과 테이블을 이리저리 살펴본다. 언뚯 보면 View() 함수를 활용해 데이터프레임을 살펴보는 것과 유사한 것처럼 보이지만, 부가적인 기능이 제공된다.

DT 패키지는 테이블의 상호작용성과 관련하여 몇 가지 기능을 제공한다.

  • Pagination: 페이지를 이동할 수 있는 기능

  • Instant search: 즉각적인 찾기 기능(Search에 타이핑하기 시작하면 즉각적으로 검색 결과 보여줌)

  • Multi-column ordering: 다중 컬럼 정렬 기능(컬럼 하나를 선택한 후 ctrl을 누른 상태에서 다른 컬럼을 선택)

  • Filtering: 값을 정렬할 수 있는 기능

  • Editable: 셀 값을 수정할 수 있는 기능

  • Buttons: 셀 숨기기, CSV, PDF, XLSX 등의 확장자로 내보내기 등을 수행하는 버튼 생성 기능

그 중 몇 가지 기능을 여기에서 살펴본다. 자세한 사항은 DT 패키지 홈페이지에 잘 정리되어 있다.

2.1 테이블 CSS 클래스

datatable() 함수의 class 인수를 통해 테이블의 외관을 바꿀 수 있다. 다음과 같은 옵션이 가능하다.

Class name Description
display stripe, hover, row-border, order-column을 동시 적용한 디폴트
cell-border 모든 셀의 상하좌우에 경계선 표시
compact 여백 축소
hover 마우스의 위치에 따라 점멸 효과
nowrap 줄바꿈 없이 텍스트 표시
order-column 정렬의 키가 되는 컬럼에 하이라이트 표시
row-border 행별 경계선 표시
stripe 행을 스트라이프로 표시

아래와 같이 cell-bordercompact를 함께 실행해 보고 테이블의 변화를 확인한다.

datatable(head(gapminder), class = "cell-border compact")

또한 특정 컬럼의 정렬 방식(왼편, 오른편, 중앙)을 변경할 수 있다. 사용가능한 옵션은 다음과 같다.

Class name Description
dt[-head|-body]-left 왼편 정렬
dt[-head|-body]-center 가운데 정렬
dt[-head|-body]-right 오른편 정렬
dt[-head|-body]-justify 양쪽 맞춤
dt[-head|-body]-nowrap 줄바꿈 없는 맞춤

아래는 첫 번째와 두 번째 컬럼(country, continent)의 내용(body)을 가운데 정렬로 나타낸다.

datatable(head(gapminder),
          options = list(
            columnDefs = list(list(className = "dt-body-center", targets = 1:2))
          ))

2.2 테이블 에디팅

editable 인수를 통해 테이블의 값을 수정할 수 있게 만들 수 있다. 테이블의 특정 셀에 더블클릭하면 수정할 수 있다.

datatable(head(gapminder), editable = "cell")

2.3 컬럼 필터

다음과 같은 방식으로 필터를 설정할 수 있다.

datatable(gapminder, filter = "top", 
          options = list(
            pageLength = 5, 
            autoWidth = TRUE
          ))

2.4 버튼 기능

extenstion에 Buttons, dom에 Bftip, buttonsc("copy", "excel", "pdf", "print")를 입력하면 버튼 기능을 활성화할 수 있다. 각 인수가 무엇을 의미하는지는 다음의 웹사이트를 참고할 수 있다.

datatable(gapminder, filter = "top",
          extensions = "Buttons",
          options = list(
            pageLength = 5,
            autoWidth = TRUE,
            dom = "Bfrtip",
            buttons = c("copy", "excel", "pdf", "print")
          ))

3 그래프

3.1 인터랙티브 그래프

인터랙티브 시각화 도구로 최근 널리 각광을 받고 있는 Plotly이다. Plotly는 사실 캐나다 퀘백에 본사를 두고 있는 데이터 시각화 전문 회사 이름이다. 그러나 보통 데이터 시각화용 JavaScript 라이브러리를 일컽는다. 이 라이버러리는 다양한 오픈소스 프로그래밍 언어에서 사용가능하며(이기준 2023), R의 랩퍼 프로그램이 plotly 패키지이다.

gapminder 데이터를 이용하여 간단한 그래프를 그려보자. 문법이 ggplot2와 크게 다르지 않음을 알 수 있다.

gapminder |> 
  filter(year == 2007) |> 
  plot_ly(x = ~gdpPercap, y = ~lifeExp, color = ~continent,
          text = ~paste("Country: ", country, 
                        "<br>GDP per capita: ", gdpPercap, 
                        "$<br>Life Expectancy at Birth:", lifeExp))

줌(zoom), 팬(pan), 박스 선택(box select), 라소 선택(Lasso select), 줌인(zome in), 줌 아웃(zoom out) 등과 같은 상호작용 기능을 확인할 수 있다. 또 그래프 상의 데이터 포인트 위에 마우스를 올리면 text 인수를 통해 설정한 내용을 볼 수 있다. 그리고 범례를 클릭하면 특정 continent의 국가를 나타나지 않게 할 수 있다.

이와 같이 plotly 패키지를 직접 사용하면 다양한 기능을 활용할 수 있겠지만, plotly 패키지가 제공하는 ggplotly() 함수를 활용하면 ggplot2로 만들어진 그래프를 단숨에 plotly 그래프로 바꿀 수 있다. 물론 정확히 같지는 않다.

P <- gapminder |> 
  filter(year == 2007) |> 
  ggplot(aes(x = gdpPercap, y = lifeExp, color = continent)) +
  geom_point() + 
  scale_color_brewer(palette = "Set2") +
  theme_minimal()
ggplotly(P)

다음 예제 역시 반응형이지만 바로 다음에서 다룰 동적인 특성도 동시에 가지고 있는 그래프를 만드는 것이다. 역시 plotly 패키지를 이용한다.

gapminder |> 
  plot_ly(x = ~log10(gdpPercap), y = ~lifeExp,
          text = ~paste("Country: ", country)) |> 
  add_markers(color = ~continent, size = ~pop, frame = ~year, 
              marker = list(sizeref = 0.2, sizemode = "area"))

하단에 있는 ‘Play’ 버튼을 누르면 연도에 따라 그래프가 바뀌면서 동적인 효과가 나타나게 된다.

3.2 애니메이션 그래프

이 실습에서는 gganimate 패키지를 활용하여, 움직이는 그래프를 만드는 방법을 익히도록 한다. 물론 움직인다고 해서 인터랙티브한 그래프인 것은 아니다. gganimate 패키지를 설치한 후 불러온다.

우선 정적인 그래프를 그린다.

P <- gapminder |> 
  ggplot(aes(x = gdpPercap, y = lifeExp, size = pop, color = continent)) +
  geom_point(show.legend = FALSE, alpha = 0.7) +
  scale_x_log10() +
  scale_size(range = c(2, 12))
P

이 그래프는 두 변수 간에 양적인 관련성이 있다는 사실은 명백히 보여주지만, 데이터 변형의 측면에서는 잘못된 것이다. 모든 연도(1952~2007년간 5년 단위)가 나타나 있어서 한 국가가 그래프에 12번 등장한다.

이를 해결하기 위해 ggplot2 패키지의 facet_wrap() 함수를 활용한다.

P + facet_wrap(~year)

이 그래프는 두 변수간의 양적인 상관관계가 12개 모두의 연도에서 나타난다는 사실을 명확히 보여준다. 그러나 그래프를 세세히 살펴보면 알 수 있듯이, 두 변수의 관련성이라는 측면에서 개별 국가가 시간의 흐름에 따라 어떻게 변화해 나가는지에 대한 사항을 파악하기는 매우 어렵다.

gganimate 패키지의 transition_time() 함수를 활용하여 동적인 그래프를 작성해 본다.

P + transition_time(year) +
  labs(title = "Year: {frame_time}")

대륙별로 분할하여 표현할 수도 있다.

P + facet_wrap(~continent) +
  transition_time(year) +
  labs(title = "Year: {frame_time}")

움직임을 조금 더 역동적이게 만들어 볼 수 있다.

P + transition_time(year) +
  labs(title = "Year: {frame_time}") +
  shadow_wake(wake_length = 0.1, alpha = FALSE)

그래프를 저장하고 싶으면 anim_save() 함수를 활용할 수 있다. ggsave() 함수와 동일한 문법을 갖는다.

4 지도

4.1 벡터 데이터와 sf 패키지

지도는 간단히 말해 지리공간적(geospatial) 데이터를 그래픽 형태로 나타낸 것이다. 따라서 지리공간적 데이터를 이해하는 것이 지도 제작의 시발점이 되어야 한다. 지리공간적 데이터는 GIS적 관점에서 벡터(vector) 데이터와 래스터(raster) 데이터로 나뉜다. 벡터 데이터는 지리공간적 사상의 기하학적 형태(형상 데이터)와 그것의 다양한 특성(속성 데이터)을 담고 있는 데이터이다.

벡터 데이터는 지리공간적 사상을 포인트, 라인, 폴리곤 피처로 구분하고, 개별 피처를 형태와 위치를 개별 피처를 구성하고 있는 버택스의 좌표값을 저장함으로써 구현하는데, 이러한 정보를 담고 있는 것을 형상 데이터라고 한다. 예를 들어 우리나라 17개 시도에 대한 디지털 행정구역도가 여기에 해당한다. 이에 반해 속성 데이터는 포인트, 라인, 폴리곤 등으로 재현된 개별 피처의 다양한 특성을 가지고 있는 것으로 보통 테이블 데이터라고 한다. 예를 들어, 우리나라 17개 시도의 인구수, 인구성장률, 순이동률과 같은 것이다. 형상 데이터가 지리공간적 데이터의 특수성을 더 잘 반영하기 때문에 보통 형상 데이터를 공간 데이터라고도 하면, 벡터 데이터에서는 이 두 종류의 데이터가 기본적으로 독립적이며 보통은 느슨한 형태로 결합되어 있다.

이에 반해 래스터 데이터는 세상을 동일한 크기의 수 많은 그리드 셀(grid cell)로 구성되어 있다고 보며, 개별 그리드 셀에 속성이 저장되어 있는 데이터를 의미한다. 가장 쉬운 예가 인공위성 영상이다. 인공위성 영상의 특정한 공간해상도를 가진 픽셀로 나뉘어져 있고, 개별 픽셀에 특정한 값(특정한 밴드의 반사값)이 들어가 있다. 따라서 래스터 데이터는 벡터 데이터처럼 형상 데이터와 속성 데이터가 분리되어 있는 것이 아니라 일체형이다. 특수한 경우가 아니라면 하나의 래스터 파일에는 하나의 속성만이 들어가 있다. 이에 반해 벡터 데이터의 속성 파일에는 수많은 변수가 포함될 수 있다.

지도 제작의 원칙은 동일하지만, 지도로 나타낼 데이터가 벡터 데이터인지 래스터 데이터인지에 혹은 둘 다인지에 따라 지도화의 세부 절차는 달라질 수 있다. 여기서는 벡터 데이터에 기반한 지도 제작에 집중하고자 한다. 속성 데이터는 기본적으로 R의 데이터 프레임과 동일한 개념이므로, 데이터사이언스의 기본 과정을 통해 불러오고, 정돈하고, 변형할 수 있다. 따라서 보다 중요한 것은 형상 데이터를 다루는 것이다.

벡터 데이터 포맷, 보다 정확하게는 형상 데이터의 포맷으로 가장 널리 사용되고 있는 것이 셰이프 파일(shape file)이다. 셰이프 파일(확장자가 .shp인 파일)은 전세계에서 가장 큰 GIS 회사인 ESRI가 오래전에 개발한 벡터 데이터 포맷으로, 현재 표준 포맷의 역할을 하고 있다. 그런데 셰이프 파일은 동일한 이름을 공유하지만 확장자가 서로 다른 몇 개의 파일의 묶음을 지칭한다는 점을 이해할 필요가 있다. 반드시 다음의 네 파일을 함께 가지고 있어야 한다.

  • *.shp: 버텍스의 좌표값이 포함된 핵심 파일

  • *.dbf: 기본 속성 파일

  • *.shx: 공간적 인덱싱 파일

  • *.prj: 투영 정보 파일

마지막의 *.prj 파일은 없어도 지도로 나타날 수는 있다. 그러나 다른 셰이프 파일과 함께 지도로 나타내거나 축척막대와 같은 지도 요소를 적절하게 나타내기 위해서는 좌표참조계(CRS) 정보가 포함된 *.prj 파일은 가질 필요가 있다.

R에서 형상 데이터를 다루는데 있어 거의 표준처럼 사용되고 있는 것이 sf 패키지이다. 기본적으로는 셰이프 파일을 불러오기 위한 st_read() 함수를 주로 사용하게 되겠지만, sf 패키지는 벡터-기반 GIS 오퍼레이션을 위한 폭넓은 함수를 제공한다. 중요한 것을 정리하면 다음과 같다.

구분 함수 설명
읽고 쓰기

st_read()

read_sf()

셰이프 파일 읽어 들이기
st_write() 셰이프 파일 저장
투영 관련 st_crs() CRS 정보 확인
st_transform() CRS 바꾸기
기하 측정 st_area() 면적 계산
st_length() 길이 계산
st_perimeter() 둘레 계산
st_distance() 거리 계산
기하 변형 st_centroid() 센트로이드 생성
st_buffer() 버퍼 생성
st_boundary() 가장자리 추출
st_simplify() 선 피처 단순화 실행
기하 생성 st_point() 포인트 피처 생성
st_vironoi() 보로노이 폴리곤 생성
st_convex_hull() 컨벡스 헐 생성
st_make_grid() 규칙 그리드 생성
기하 검토 st_is_valid() 지오메트리가 밸리드한지 여부 검토
st_make_valid() 지오메트리를 밸리드하게 만들기
기하 중첩 st_intersection() 기하 교집합 중첩
st_union() 기하 합집합 중첩
st_crop() 기하 크롭 중첩
기타 st_coordinates() 버택스 좌표값 반환
st_cast() 다른 피처 유형으로 변환
st_as_sf() sf 객체로 변환
st_graticule() 그래티큘 생성
st_join() 공간적 조인 실행

래스터 데이터를 다루는데는 terra 패키지가 가장 널리 사용되고 있으며, stars패키지가 최근 많은 주목을 받고 있다. stars 패키지와 sf 패키지는 모두 에트저르 페베스마(Edzer Pebesma)가 만들었다. 두 패키지에 대한 설명은 페베스마와 로저 비번드(Roger Bivand)가 함께 쓴 ’R을 활용한 공간데이터사이언스(Spatial Data Science With Applications in R)’에 잘 나타나 있다(Pebesma 와/과 Bivand (2023))

4.2 정적 지도 제작

4.2.1 세계 지도

ggplot2 패키지를 이용하여 정적 지도를 그려본다. ggplot2패키지로 지도를 그린다는 것은 ’지도도 그래프다.’라는 접근법에 기반하고 있다. ggplot2의 문법을 지도 제작으로 확장할 수 있고, 막강한 생태계를 고려할 때 충분히 이점이 있는 접근법이다.

데이터는 지난 실습에서 사용한 WPP 2024(World Population Prospects 2024)이다. 2024년 전세계 국가별 TFR(Total Fertility Rate, 합계출산율) 지도를 그려본다.

벡터 데이터를 활용한 지도는 형상 데이터와 속성 데이터를 결합해야만 제작할 수 있다. 여기서 형상 데이터는 전세계 국가 경계 데이터이고, 속성 데이터는 TFR이 포함된 WPP 2024 데이터이다. 형상 데이터는 spData 패키지에 들어 있는 world 객체를 사용한다. 벡터 형식의 데이터는 sf 패키지의 st_as_sf() 함수를 통해 sf 객체로 변환하는 것이 좋다.

library(spData)
library(sf)
data(world)
world <- st_as_sf(world)

WPP 2024 데이터를 불러와 2025년만 골라낸다.

wpp_2024 <- read_rds("wpp_2024.rds")
my_wpp <- wpp_2024 |> 
  filter(year == 2025)

두 데이터를 left_join() 함수를 이용하여 결합한다. 벡터 데이터의 경우는 늘 반드시 형상 데이터를 중심에 두고 left_join() 함수를 통해 속성 데이터를 불러와 합체해야 한다.

world_data <- world |>
  left_join(my_wpp, join_by(iso_a2 == ISO2))

로빈슨 도법(Robinson projection)의 지도를 제작한다. ggplot2 패키지로 지도를 그리는 가장 좋은 방법은 기하객체 함수인 geom_sf()와 좌표 변환 함수인 coord_sf()를 결합하는 것이다. scale_x_continuous()scale_y_continuous()의 내용은 그래티큘(경위선망)을 원하는 방식대로 지도에 포함시키기 위한 것이다. 그래프를 world_map이라는 이름의 객체로 저장하는 것은 뒤에서 이 지도를 사용하기 때문이다.

world_map <- ggplot() +
  geom_sf(data = world_data, aes(fill = TFR, text = name_long)) +
  coord_sf(crs = "+proj=robin") +
  scale_fill_viridis_c() +
  scale_x_continuous(breaks = seq(-180, 180, 30)) +
  scale_y_continuous(breaks = c(-89.5, seq(-60, 60, 30), 89.5)) +
  theme(
    panel.background = element_rect("white"),
    panel.grid = element_line(color = "gray80")
  )
world_map

4.2.2 우리나라 지도

우리나라 지도도 그려본다. ’Lab07: 데이터 수집하기’에서 KOSIS의 API를 통해 수집, 정리한 시군구 단위 지역소멸위험지수를 지도화한다. 우선 우리나라 시군구 행정 경계에 대한 도형(형상, 기하) 데이터가 필요하다. GADM(Database of Global Administrative Areas)는 전 세계 국가별, 행정구역 수준별, 고해상도 디지털 경계 파일을 제공한다. 직접 웹사이트를 방문해 파일을 다운로드할 수도 있지만, 여기서는 geodata 패키지의 도움을 받는다. geodata 패키지는 정말 다양한 종류의 공간 데이터를 쉽게 다운로드할 수 있게 도와준다. 1레벨인 시도 단위와 2레벨인 시군구 단위를 다운받는다. path 아규먼트가 반드시 필요한데, 프로젝트 파일의 폴더도 좋고, 앞으로 geodata 패키지를 통해 다운받을 데이터를 모아둘 곳을 따로 정해두어도 좋다. 그렇지만 할당 연산자를 통해 이름을 부여하면 따로 다운로드된 파일을 다시 부르는 수고는 하지 않아도 된다.

library(geodata)
library(tmap)

korea_sd <- gadm(country = "South Korea", level = 1, path = "D:/My R/Geodata/")
korea_sd <- korea_sd |> 
  st_as_sf()
korea_sgg <- gadm(country = "South Korea", level = 2, path = "D:/My R/Geodata/")
korea_sgg <- korea_sgg |> 
  st_as_sf()

그런데 파일을 열어보고서 경악한다. 행정구역 코드가 없다. 그래서 이 방법은 포기한다.

행정구역 파일은 통계청의 통계지리정보서비스에서 구할 수 있다. 다운받는 방법을 익히면 좋겠지만, 시간 절약을 위해 다운받아 정리한 파일을 그냥 제공한다. 프로젝트 폴더에 파일을 저장한 후, 아래의 코드를 통해 불러온다.

sido_shp <- st_read("sido.shp", options = "ENCODING=CP949")
sigungu_shp <- read_sf("sigungu.shp", options = "ENCODING=CP949")

두 파일에 대해 서로 다른 함수를 적용한 것을 알 수 있다. st_read() 함수는 가장 보편적으로 사용되는 것으로 불러올 때마다 파일에 대한 정보(지오메트리 유형, 바운딩 박스, CRS 등)가 자동으로 디스플레이된다. 이러한 정보는 항상 유익한 것이지만 보이지 않게 하고 싶을 수도 있다. 이 때 read_sf() 함수를 사용할 수 있다.

불러들인 파일을 바탕으로 시군구 경계를 그려본다. 지도 제작 전문 패키지인 tmapqtm() 함수를 이용하여 시군구 경계에 대한 지도를 빠르게 그려본다. tmap패키지로 지도를 그린다는 것은 ’지도는 지도다.’라는 접근법에 기반하고 있다. tmap의 문법을 새로 배워야 한다는 단점이 있긴 하지만, 지도는 그래프로 환원될 수 없는 고유한 특성이 있고, tmap패키지는 이러한 지도의 고유한 특성을 잘 반영하고 있다. 좀 더 복잡한 tmap의 문법을 사용한 지도 제작은 맨 뒤에서 다루기로 한다.

library(tmap)
qtm(sigungu_shp)

지역소멸위험지수 데이터를 불러온다. 아래 코드는 지난번 실습 때 rds 파일 포맷으로 저장해 둔 것을 가정한 것이다. 실습의 편의를 위해 파일을 제공하니 프로젝트 폴더에 저장한 후, 아래의 코드를 통해 불러온다.

data_sigungu <- read_rds("data_sigungu.rds")

도형 데이터(korea_sgg)와 속성 데이터(data_sigungu)를 공통 키(key)를 활용하여 결합한다.

sigungu_data <- sigungu_shp |> 
  left_join(
    data_sigungu, join_by(SGG1_CD == C1)
  )

이제 ggplot2 패키지를 이용하여 지도를 제작한다. ’Lab07: 데이터 수집하기’에서 인구소멸위험지수의 시도별 그래프를 제작한 것과 비교해 보라. 그 유사함에 깜짝 놀랄 수도 있다. ggplot2에서는 그래프와 지도의 구분이 없다. 이것은 ggplot2의 장점이자 단점이다.

sigungu_data <- sigungu_data |> 
  mutate(
    index_class = case_when(
      index < 0.2 ~ "1",
      index >= 0.2 & index < 0.5 ~ "2",
      index >= 0.5 & index < 1.0 ~ "3",
      index >= 1.0 & index < 1.5 ~ "4",
      index >= 1.5 ~ "5"
    ),
    index_class = fct(index_class, levels = as.character(1:5))
  )

class_color <- c("1" = "#d7191c", "2" = "#fdae61",
                 "3" = "#ffffbf", "4" = "#a6d96a", 
                 "5" = "#1a9641")
ggplot() +
  geom_sf(data = sigungu_data, aes(fill = index_class), show.legend = TRUE) +
  geom_sf(data = sido_shp, fill = NA, lwd = 0.5) +
  scale_fill_manual(name = "Classes", 
                    labels = c("< 0.2", "0.2 ~ 0.5", "0.5 ~ 1.0", 
                               "1.0 ~ 1.5", ">= 1.5"), 
                    values = class_color, drop = FALSE) 

4.3 인터랙티브 지도 제작

위에서 사용한 plotly 패키지의 ggplotly() 함수를 활용하면 반응형 지도를 생성할 수 있다. 앞의 코드 둘째 줄에 aes()text = name_long이 설정되어 있는데, 마우스로 국가를 가리킬 때 이름이 나타날 수 있게 조치한 것이다.

ggplotly(world_map)

지도 위에서 plotly 가 제공하는 다양한 기능을 적용해 볼 필요가 있다. 인터랙티브 그래프에 비해 인터랙티브 지도의 유용성이 더 높아 보인다.

우리나라 지도는 다른 방식으로 반응형으로 만들어 본다. 여기서는 ggiraph 패키지를 사용한다. 처음 사용하는 경우라면 먼저 패키지를 인스톨해야 한다. 코드의 전반부는 커서를 특정 시군구 위에 올렸을 때 나타나는 정보를 좀 더 다양하게 하려는 조치이다. 중간의 코드가 핵심인데, 찬찬히 살펴보면 그렇게 복잡하지 않다. 마지막은 완전히 지엽적인 것인데, 커서를 특정 시군구 위에 올렸을 때 색이 회색으로 변하게 하기 위한 것이다.

library(ggiraph)
sigungu_data <- sigungu_data |> 
  mutate(
    index = format(index, digits = 4, nsmall = 4),
    my_tooltip = str_c("Name: ", SGG1_FNM, "\n Index: ", index)
  )
gg <- ggplot() +
  geom_sf_interactive(
    data = sigungu_data, 
    aes(
      fill = index_class, 
      tooltip = my_tooltip, 
      data_id = SGG1_FNM
      ), 
    show.legend = TRUE) +
  geom_sf(data = sido_shp, fill = NA, lwd = 0.5) +
  scale_fill_manual(
    name = "Classes", 
    labels = c("< 0.2", "0.2 ~ 0.5", "0.5 ~ 1.0", "1.0 ~ 1.5", ">= 1.5"), 
    values = class_color, drop = FALSE) 
girafe(ggobj = gg) |> 
  girafe_options(
    opts_hover(css = "fill: gray")
  )

그러나 반응형 지도 제작에 가장 널리 쓰이는 것은 leaflet이다. leaflet은 웹 상의 반응형 지도 제작에 특화된 JavaScript 라이브러리이다. 이 라이브러리를 R에서 쓸 수 있게 도와주는 래퍼 패키지가 leaflet 패키지이다. 패키지 홈페이지가 매우 상세하게 잘 되어 있다. 숙독하기를 권한다.

매우 단순한 인터랙티브 지도를 만들어 본다. 자신이 원하는 경위도값과 설명문으로 수정하면 된다.

leaflet() |> 
  addTiles() |> 
  addPopups(126.955184, 37.460422, "Sang-Il's Office",
            options = popupOptions(closeButton = FALSE))

위에서 작성했던 TFR 세계지도를 leaflet 패키지의 다양한 함수와 아규먼트를 활용하여 인터랙티브 지도를 제작해 본다.

world_data <- world_data |> filter(!is.na(TFR))

bins <- c(0, 1.5, 2.1, 3, 4, 5, Inf)
pal <- colorBin("YlOrRd", domain = world_data$TFR, bins = bins)
labels <- sprintf("<strong>%s</strong><br/>%g",
  world_data$name_long, world_data$TFR) |> lapply(htmltools::HTML)

leaflet(world_data) |> 
  addProviderTiles(providers$Esri.WorldTopoMap) |> 
  addPolygons(
    fillColor = ~pal(TFR),
    weight =  2, 
    opacity = 1,
    color = "white",
    dashArray = "3",
    fillOpacity = 0.6,
    highlightOptions = highlightOptions(
      weight = 5,
      color = "#666",
      dashArray = "",
      fillOpacity = 0.6,
      bringToFront = TRUE),
    label = labels,
    labelOptions = labelOptions(
      style = list("font-weight" = "normal", padding = "3px 8px"),
      textsize = "15px",
      direction = "auto")
  ) |> 
  addLegend(
    pal = pal, values = ~TFR, opacity = 0.6, title = NULL,
    position = "bottomright"
  )

우리나라 시군구 단위의 인구소멸위험지수에 대한 지도를 반응형으로 만들어 본다. 여기서는 tmap을 활용한다. 해당 시군구 위에 클릭하면 지역소멸위험지수가 나타난다.

class_color <- c("#d7191c", "#fdae61", "#ffffbf", "#a6d96a", "#1a9641")
sigungu_data <- sigungu_data |> 
  mutate(
    index = as.numeric(index)
  )
tmap_mode(mode = "view")
my_tmap <- tm_shape(sigungu_data) + 
  tm_polygons(
    col = "index",
    palette = class_color, 
    breaks = c(0, 0.2, 0.5, 1.0, 1.5, Inf), 
    labels = c("< 0.2", "0.2~0.5", "0.5~1.0", "1.0~1.5", ">= 1.5"),
    title = "Classes", 
    popup.vars=c("지역소멸위험지수: " = "index"), 
    popup.format = list(index = list(digits = 3)), 
    id = "SGG1_FNM", 
    alpha = 0.6, 
    border.alpha = 0.5
  ) +
  tm_shape(sido_shp) + tm_borders(lwd = 2)
my_tmap
tmap_save(my_tmap, "지방소멸위험지수.html")
맨 위로

참고문헌

Dougherty, Jack, 와/과 Ilya Ilyankou. 2021. Hands-on data visualization: interactive storytelling from spreadsheets to code. Boston: O’Reilly Media, Inc.
Pebesma, Edzer, 와/과 Roger Bivand. 2023. Spatial Data Science: With Applications in R. 1st edition. Boca Raton, FL: Chapman; Hall/CRC.
이기준. 2023. Plotly로 시작하는 인터랙티브 데이터 시각화 in R & 파이썬. 파주: 제이펍.